
import eventFactory from './eventFactory';
import DataTransfer from './DataTransfer';

function _noop() { }

function parseParams(targetElement: any, eventProperties: any, configCallback: any) {
  if (typeof eventProperties === 'function') {
    configCallback = eventProperties;
    eventProperties = null;
  }

  if (!targetElement || typeof targetElement !== 'object') {
    throw new Error('Expected first parameter to be a targetElement. Instead got: ' + targetElement);
  }

  return {
    targetElement: targetElement,
    eventProperties: eventProperties || {},
    configCallback: configCallback || _noop
  };
}

function customizeEvent(event: any, configCallback: any, isPrimaryEvent: any) {
  if (configCallback) {
    // call configCallback only for the primary event if the callback takes less than two arguments
    if (configCallback.length < 2) {
      if (isPrimaryEvent) { configCallback(event); }
    }
    // call configCallback for each event if the callback takes two arguments
    else {
      configCallback(event, event.type);
    }
  }
}

function createAndDispatchEvents(targetElement: any, eventNames: string[], primaryEventName: any, dataTransfer: any, eventProperties: any, configCallback: any) {
  eventNames.forEach((eventName) => {
    let event = eventFactory.createEvent(eventName, eventProperties, dataTransfer);
    let isPrimaryEvent = eventName === primaryEventName;

    customizeEvent(event, configCallback, isPrimaryEvent);

    targetElement.dispatchEvent(event);
  });
}



class DragDropAction {
  lastDragSource: any = null;
  lastDataTransfer: any = null;
  pendingActionsQueue: any[] = [];

  _queue(fn: any) {
    this.pendingActionsQueue.push(fn);

    if (this.pendingActionsQueue.length === 1) {
      this._queueExecuteNext();
    }
  }

  _queueExecuteNext() {
    if (this.pendingActionsQueue.length === 0) { return; }

    let firstPendingAction = this.pendingActionsQueue[0];

    let doneCallback = () => {
      this.pendingActionsQueue.shift();
      this._queueExecuteNext();
    };

    if (firstPendingAction.length === 0) {
      firstPendingAction.call(this);
      doneCallback();
    } else {
      firstPendingAction.call(this, doneCallback);
    }
  }

  dragStart(targetElement: any, eventProperties: any, configCallback: any) {
    let params = parseParams(targetElement, eventProperties, configCallback)
      , events = ['mousedown', 'dragstart', 'drag']
      , dataTransfer = new DataTransfer();

    this._queue(() => {
      createAndDispatchEvents(params.targetElement, events, 'drag', dataTransfer, params.eventProperties, params.configCallback);

      this.lastDragSource = targetElement;
      this.lastDataTransfer = dataTransfer;
    });

    return this;
  }

  dragEnter(overElement: any, eventProperties: any, configCallback: any) {
    let params = parseParams(overElement, eventProperties, configCallback)
      , events = ['mousemove', 'mouseover', 'dragenter'];

    this._queue(() => {
      createAndDispatchEvents(params.targetElement, events, 'dragenter', this.lastDataTransfer, params.eventProperties, params.configCallback);
    });

    return this;
  }

  dragOver(overElement: any, eventProperties: any, configCallback: any) {
    let params = parseParams(overElement, eventProperties, configCallback)
      , events = ['mousemove', 'mouseover', 'dragover'];

    this._queue(() => {
      createAndDispatchEvents(params.targetElement, events, 'drag', this.lastDataTransfer, params.eventProperties, params.configCallback);
    });

    return this;
  }

  dragLeave(overElement: any, eventProperties: any, configCallback: any) {
    let params = parseParams(overElement, eventProperties, configCallback)
      , events = ['mousemove', 'mouseover', 'dragleave'];

    this._queue(() => {
      createAndDispatchEvents(params.targetElement, events, 'dragleave', this.lastDataTransfer, params.eventProperties, params.configCallback);
    });

    return this;
  }

  drop(targetElement: any, eventProperties: any, configCallback: any) {
    let params = parseParams(targetElement, eventProperties, configCallback);
    let eventsOnDropTarget = ['mousemove', 'mouseup', 'drop'];
    let eventsOnDragSource = ['dragend'];

    this._queue(() => {
      createAndDispatchEvents(params.targetElement, eventsOnDropTarget, 'drop', this.lastDataTransfer, params.eventProperties, params.configCallback);

      if (this.lastDragSource) {
        // trigger dragend event on last drag source element
        createAndDispatchEvents(this.lastDragSource, eventsOnDragSource, 'drop', this.lastDataTransfer, params.eventProperties, params.configCallback);
      }
    });

    return this;
  };

  then(callback: any) {
    this._queue(() => { callback.call(this); });    // make sure _queue() is given a callback with no arguments

    return this;
  }

  delay(waitingTimeMs: number) {
    this._queue((done: any) => {
      window.setTimeout(done, waitingTimeMs);
    });

    return this;
  }

}

export default DragDropAction;
